import os
import sys
import time
from typing import Union, Callable, IO, List, Tuple


class HttpHelper(object):
    _SIZE_UNIT = (
        '',
        'K',
        'M',
        'G',
        'T'
    )

    _TIME_UNIT = (
        's',
        'm',
        'h'
    )

    @staticmethod
    def buffer_receiver(data_buffer: List[Tuple[int, bytes]]) -> Callable[[int, bytes, int], None]:
        def on_receive(offset: int, data: bytes, file_size: int):
            data_buffer.append((offset, data))

        return on_receive

    @staticmethod
    def path_receiver(path: str) -> Callable[[int, bytes, int], None]:
        file_holder: List[Union[None, IO]] = [None]

        def on_receive(offset: int, data: bytes, file_size: int):
            # open file for fist time
            file = file_holder[0]
            if file is None:
                # create and resize file
                with open(path, 'wb') as f:
                    if file_size > 0:
                        f.truncate(file_size)

                # open file
                file_holder[0] = file = open(path, 'rb+')

            # close file when eos
            if offset == file_size:
                file.flush()
                file.close()
                return

            # write file
            file.seek(offset, os.SEEK_SET)
            file.write(data)

        return on_receive

    @staticmethod
    def file_receiver(file: IO) -> Callable[[int, bytes, int], None]:
        initialized = [False]

        def on_receive(offset: int, data: bytes, file_size: int):
            # first data
            if not initialized[0]:
                initialized[0] = True
                if file_size > 0:
                    file.truncate(file_size)

            # eos
            if offset == file_size:
                return

            # write to file
            file.seek(offset, os.SEEK_SET)
            file.write(data)

        return on_receive

    @staticmethod
    def cmd_progress_bar(label: str = '',
                         progress_length: int = 50,
                         max_update_interval_ms: int = 1000) -> Callable[[int, int], None]:
        last_progress = [-1]
        start_time = [0.0]
        last_time = [0.0]
        speed_counter = []
        speed_duration = 10.0
        max_update_interval = max_update_interval_ms / 1000

        def on_progress(c: int, m: int):
            cur_time = time.time()
            progress_cur = (c * progress_length) // m if m >= 0 else -1
            if progress_cur > progress_length:
                progress_cur = progress_length

            if progress_cur != last_progress[0] or (cur_time - last_time[0] > max_update_interval):
                # store last time
                last_time[0] = cur_time

                # store start time
                if last_progress[0] == -1:
                    start_time[0] = time.time()

                # generate speed
                speed_counter.append((cur_time, c))
                while cur_time - speed_counter[0][0] > speed_duration:
                    del speed_counter[0]  # remove records earlier than speed_duration
                if len(speed_counter) <= 1:
                    speed = 0
                    speed_str = '0K/s'
                else:
                    t_first, b_first = speed_counter[0]
                    t_last, b_last = speed_counter[len(speed_counter) - 1]
                    delta_b = b_last - b_first
                    delta_t = t_last - t_first
                    if delta_t > 0:
                        speed = delta_b / delta_t
                        speed_str = f'{HttpHelper._format_size(speed)}/s'
                    else:
                        speed = 0
                        speed_str = '0K/s'

                # update last progress
                last_progress[0] = progress_cur

                # generate progress string
                progress_str = ('=' * progress_cur) + (' ' * (progress_length - progress_cur))

                # print progress bar
                sys.stdout.write('\r')
                sys.stdout.write(f'{label}: [{progress_str}]'
                                 f' {(c*100)//m if m >= 0 else "-"}%'
                                 f', {speed_str}'
                                 f', {HttpHelper._format_size(c)}/{HttpHelper._format_size(m) if m >= 0 else "-"}'
                                 f', {HttpHelper._formate_time(cur_time - start_time[0])}'
                                 f'/{HttpHelper._formate_time((m - c) / speed) if speed > 0 else "-"}')

                if c == m:
                    sys.stdout.write('\n')

                sys.stdout.flush()

        return on_progress

    @staticmethod
    def _format_size(size: Union[int, float]) -> str:
        i_unit = 0
        while size >= 1024 and i_unit < (len(HttpHelper._SIZE_UNIT) - 1):
            size /= 1024
            i_unit += 1
        return f'{size:.2f}{HttpHelper._SIZE_UNIT[i_unit]}'

    @staticmethod
    def _formate_time(t: Union[int, float]) -> str:
        if not isinstance(t, int):
            t = int(t)

        time_strs = []
        i_unit = 0
        while t > 0 and i_unit < len(HttpHelper._TIME_UNIT):
            time_strs.append(f'{t % 60}{HttpHelper._TIME_UNIT[i_unit]}')
            t //= 60
            i_unit += 1

        if len(time_strs) == 0:
            time_strs.append(f'0{HttpHelper._TIME_UNIT[0]}')

        time_strs.reverse()
        return ''.join(time_strs)
